﻿using ErrorDumper.Configuration;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;

namespace ErrorDumper.Writing {
    public static class ExceptionWriters {

        private static bool _isLoaded;
        private static readonly object _syncRoot = new object();
        private static readonly List<IExceptionWriter> _writers = new List<IExceptionWriter>();

        public static IEnumerable<IExceptionWriter> GetWriters() {
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                return _writers.ToList();
            }
        }

        private static void Load() {
            lock (_syncRoot) {
                if (_isLoaded)
                    return;

                _writers.Clear();

                var section = WritingConfigurationSection.GetSection();
                if (section != null)
                    foreach (WriterConfigurationElement config in section.Writers) {
                        switch (config.Reference.ToLowerInvariant()) {
                            case "":
                                var typeName = config.WriterType;
                                if (string.IsNullOrEmpty(typeName))
                                    throw new ConfigurationErrorsException(
                                        "Attributes 'ref' or 'type' must be specified.",
                                        config.ElementInformation.Source, config.ElementInformation.LineNumber);
                                var type = Type.GetType(typeName, false);
                                if (type == null)
                                    throw new ConfigurationErrorsException(string.Format(
                                        "Type '{0}' specified in attribute 'type' could not be resolved.", typeName),
                                        config.ElementInformation.Source, config.ElementInformation.LineNumber);
                                _writers.Add((IExceptionWriter)Activator.CreateInstance(type));
                                break;

                            case "sql":
                                if (IsRegisteredWriterCore<SqlExceptionWriter>())
                                    throw new ConfigurationErrorsException(string.Format(
                                        "Writer with reference '{0}' is already registered.", config.Reference),
                                        config.ElementInformation.Source, config.ElementInformation.LineNumber);
                                _writers.Add(new SqlExceptionWriter());
                                break;

                            case "directory":
                                if (IsRegisteredWriterCore<DirectoryExceptionWriter>())
                                    throw new ConfigurationErrorsException(string.Format(
                                        "Writer with reference '{0}' is already registered.", config.Reference),
                                        config.ElementInformation.Source, config.ElementInformation.LineNumber);
                                _writers.Add(new DirectoryExceptionWriter());
                                break;

                            case "trace":
                                if (IsRegisteredWriterCore<TraceExceptionWriter>())
                                    throw new ConfigurationErrorsException(string.Format(
                                        "Writer with reference '{0}' is already registered.", config.Reference),
                                        config.ElementInformation.Source, config.ElementInformation.LineNumber);
                                _writers.Add(new TraceExceptionWriter());
                                break;

                            case "eventlog":
                                if (IsRegisteredWriterCore<EventLogExceptionWriter>())
                                    throw new ConfigurationErrorsException(string.Format(
                                        "Writer with reference '{0}' is already registered.", config.Reference),
                                        config.ElementInformation.Source, config.ElementInformation.LineNumber);
                                _writers.Add(new EventLogExceptionWriter());
                                break;

                            case "console":
                                if (IsRegisteredWriterCore<ConsoleExceptionWriter>())
                                    throw new ConfigurationErrorsException(string.Format(
                                        "Writer with reference '{0}' is already registered.", config.Reference),
                                        config.ElementInformation.Source, config.ElementInformation.LineNumber);
                                _writers.Add(new ConsoleExceptionWriter());
                                break;

                            default:
                                throw new ConfigurationErrorsException(string.Format(
                                    "Writer reference '{0}' is not supported.", config.Reference),
                                    config.ElementInformation.Source, config.ElementInformation.LineNumber);
                        }
                    }

                _isLoaded = true;
            }
        }

        public static void Reset() {
            lock (_syncRoot)
                _isLoaded = false;
        }

        public static void Clear() {
            lock (_syncRoot) {
                _writers.Clear();
                _isLoaded = true;
            }
        }

        public static bool RegisterWriter<TWriter>()
            where TWriter : class, IExceptionWriter, new() {

            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                if (_writers.Any(i => i.GetType() == typeof(TWriter)))
                    return false;
                _writers.Add(new TWriter());
                return true;
            }
        }

        private static bool IsRegisteredWriterCore<TWriter>()
            where TWriter : class, IExceptionWriter, new() {

            return _writers.Any(i => i.GetType() == typeof(TWriter));
        }

        public static bool IsRegisteredWriter<TWriter>()
            where TWriter : class, IExceptionWriter, new() {

            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                return IsRegisteredWriterCore<TWriter>();
            }
        }

        private struct ExceptionEntry {
            private readonly ExceptionPropertyCollection _properties;
            private readonly string _content;

            public ExceptionEntry(ExceptionPropertyCollection properties, string content) {
                _properties = properties;
                _content = !string.IsNullOrWhiteSpace(content) ? content : "Unknown exception.";
            }

            public ExceptionEntry(string content) : this(null, content) { }

            public ExceptionPropertyCollection Properties { get { return _properties; } }
            public string Content { get { return _content; } }
        }

        public static string WriteExceptionSafely(ExceptionPropertyCollection properties, string content) {
            try {
                var errorQueue = new Queue<ExceptionEntry>();
                errorQueue.Enqueue(new ExceptionEntry(properties, content));
                string result = null;
                var isWrited = false;

                foreach (var writer in GetWriters())
                    try {
                        while (errorQueue.Count > 0) {
                            var firstError = errorQueue.Peek();
                            var writerResult = writer.WriteException(firstError.Properties, firstError.Content);
                            errorQueue.Dequeue();

                            if (!isWrited) {
                                result = writerResult;
                                isWrited = true;
                            }
                        }
                        break;
                    }
                    catch (Exception writerException) {
                        errorQueue.Enqueue(
                            new ExceptionEntry(
                                string.Format("Writer '{0}' failure: {1}", writer.GetType().FullName, writerException)));
                    }

                foreach (var error in errorQueue)
                    InternalExceptionHandler.HandleSafely("Not writed exception: " + error.Content, content);

                return result;
            }
            catch (Exception intenalException) {
                InternalExceptionHandler.HandleSafely(intenalException, content);
                return null;
            }
        }

        public static string WriteExceptionSafely(ExceptionPropertyCollection properties, Exception exception) {
            try {
                var content = exception != null ? exception.ToString() : "Unknown exception.";
                return WriteExceptionSafely(properties, content);
            }
            catch (Exception intenalException) {
                InternalExceptionHandler.HandleSafely(intenalException, exception);
                return null;
            }
        }

        public static string WriteInternalExceptionSafely(Exception internalException, object originalException) {
            InternalExceptionHandler.HandleSafely(internalException, originalException);

            try {
                var content = internalException != null
                                  ? "Internal exception: " + internalException
                                  : "Unknown internal exception.";

                return WriteExceptionSafely(null, content);
            }
            catch (Exception subInternalException) {
                InternalExceptionHandler.HandleSafely(subInternalException, internalException);
                return null;
            }
        }
    }
}
